凡猿修仙传:斩杀HardwareRenderer.nSetStopped ANR
本文作者
作者:三雒
链接:
https://juejin.cn/post/7351687181381517322
本文由作者授权发布。
ANR: COMBINED_FAIL: Application did not respond to UI input
at syscall (/apex/com.android.runtime/lib64/bionic/libc.so:28)
at __futex_wait_ex (/apex/com.android.runtime/lib64/bionic/libc.so:144)
at pthread_cond_wait (/apex/com.android.runtime/lib64/bionic/libc.so:72)
at std::__1::condition_variable::wait (/system/lib64/libc++.so:20)
at std::__1::__assoc_sub_state::copy (/system/lib64/libc++.so:84)
at std::__1::future<void>::get (/system/lib64/libc++.so:24)
at android::uirenderer::renderthread::RenderProxy::setStopped (/system/lib64/libhwui.so:364)
at android.graphics.HardwareRenderer.nSetStopped (HardwareRenderer.java:-2)
at android.graphics.HardwareRenderer.setStopped (HardwareRenderer.java:498)
at android.view.ViewRootImpl.performDraw (ViewRootImpl.java:4428)
at android.view.ViewRootImpl.performTraversals (ViewRootImpl.java:3610)
at android.view.ViewRootImpl.doTraversal (ViewRootImpl.java:2379)
at android.view.ViewRootImpl$TraversalRunnable.run (ViewRootImpl.java:9138)
at android.view.Choreographer$CallbackRecord.run (Choreographer.java:1234)
at android.view.Choreographer$CallbackRecord.run (Choreographer.java:1242)
at android.view.Choreographer.doCallbacks (Choreographer.java:902)
at android.view.Choreographer.doFrame (Choreographer.java:835)
at android.view.Choreographer$FrameDisplayEventReceiver.run (Choreographer.java:1217)
at android.os.Handler.handleCallback (Handler.java:942)
at android.os.Handler.dispatchMessage (Handler.java:99)
at android.os.Looper.loopOnce (Looper.java:201)
at android.os.Looper.loop (Looper.java:288)
at android.app.ActivityThread.main (ActivityThread.java:8061)
at java.lang.reflect.Method.invoke (Method.java:-2)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:703)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:923)
奈何此妖修为极深,从堆栈上只能看出其是在主线程渲染帧过程中,在View的三大流程的最后一步performDraw时候作案。从表面找出其破绽并不容易,但ANR率长期居高不下,用户苦其久矣,我们痛定思痛,定要斩杀这大妖。
案发现场
线索一
线索二
剖析nSetStopped
/**
* Hard stops rendering into the surface. If the renderer is stopped it will
* block any attempt to render. Calls to {@link FrameRenderRequest#syncAndDraw()} will
* still sync over the latest rendering content, however they will not render and instead
* {@link #SYNC_CONTEXT_IS_STOPPED} will be returned.
*
* <p>If false is passed then rendering will resume as normal. Any pending rendering requests
* will produce a new frame at the next vsync signal.
*
* <p>This is useful in combination with lifecycle events such as {@link Activity#onStop()}
* and {@link Activity#onStart()}.
*
* @param stopped true to stop all rendering, false to resume
* @hide
*/
public void setStopped(boolean stopped) {
nSetStopped(mNativeProxy, stopped);
}
static void android_view_ThreadedRenderer_setStopped(JNIEnv* env, jobject clazz,
jlong proxyPtr, jboolean stopped) {
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
proxy->setStopped(stopped);
}
void RenderProxy::setStopped(bool stopped) {
mRenderThread.queue().runSync([this, stopped]() { mContext->setStopped(stopped); });
}
//初始值
bool mStopped = false;
void CanvasContext::setStopped(bool stopped) {
//判断值是否改变
if (mStopped != stopped) {
mStopped = stopped;
if (mStopped) {
mGenerationID++;
mRenderThread.removeFrameCallback(this);
mRenderPipeline->onStop();
mRenderThread.cacheManager().onContextStopped(this);
} else if (mIsDirty && hasOutputTarget()) {
mRenderThread.postFrameCallback(this);
}
}
}
template <class F>
auto runSync(F&& func) -> decltype(func()) {
std::packaged_task<decltype(func())()> task{std::forward<F>(func)};
post([&task]() { std::invoke(task); });
//等待结果
return task.get_future().get();
};
既然是无效的那我们的破敌之法也就比较清晰了。
HardwareRenderer:
boolean mStopped = false;
public void setStopped(boolean stopped) {
if(mStopped != stopped){
mStopped = stopped;
nSetStopped(mNativeProxy, stopped);
}
}
剑一 :反射+动态代理
剑二:ArtMethod替换字节码
剑三:PLT hook
static const JNINativeMethod gMethods[] = {
{"nSetStopped", "(JZ)V", (void*)android_view_ThreadedRenderer_setStopped}
}
剑四: JNI hook
该思路如其名就是直接替换JNI方法,我们可以将android_view_ThreadedRenderer_setStopped替换到我们自定义的一个函数地址上。基本原理是对于Java native方法而言其ArtMethod的data_字段中保存的就是该方法对应的JNI函数地址,我们只需要替换这个地址就可以实现JNI函数的逻辑替换,具体实现可以看这篇 JNI函数 Hook实战 ,但该方案似乎没有经过大量的应用验证,考虑到稳定性暂时也没有使用该方法。
https://juejin.cn/post/7268894037464367140
剑五:Inline hook
最后终于祭出大招Inline hook, 我们知道Inline hook可以替换方法的内容,但是担心其稳定性,好在修仙界的顶流帮忙ByteDance开源了其android-inline-hook , 据说稳定性极好。
https://github.com/bytedance/android-inline-hook
void RenderProxy::setStopped(bool stopped) {
mRenderThread.queue().runSync([this, stopped]() { mContext->setStopped(stopped); });
}
void proxySetStopped(void *renderProxy, bool stop) {
SHADOWHOOK_STACK_SCOPE();
bool lastStop = findStopStatus(renderProxy);
if (stop != lastStop) {
SHADOWHOOK_CALL_PREV(proxySetStopped, renderProxy, stop);
setStopStatus(renderProxy, stop);
}
}
bool findStopStatus(void *thiz) {
std::lock_guard<std::mutex> lock(proxyMutex);
bool lastStop = false;
auto it = proxy_map.find(thiz);
if (it != proxy_map.end()) {
lastStop = it->second;
}
return lastStop;
}
void setStopStatus(void *thiz, bool stop) {
std::lock_guard<std::mutex> lock(proxyMutex);
proxy_map[thiz] = stop;
}
这里使用Map保存状态的的原因是RenderProxy是每个Window对应一个,需要分别保存,我方九剑只出五剑敌方已经元气大伤。
对于问题的探究我们始终遵从第一性原理,对于事物本身了解的越详细越透彻越我们能找到越多越合适的方案。 Android上的hook手段非常多,从时机上可以分为编译时hook和运行时hook两大类。编译时hook主要以构建过程中的字节码修改为主,代表有Lancet、ByteX等;运行时hook可以细分为Java层和Native层hook,Java层常用的有反射替换、动态代理、ArtTI等 , Native层的Hook主要就是以PLT 和 inline hook为主,还有一些特定场景使用的比如JNI hook,虚函数hook、根据内存偏移选址变量并修改等。 关于hook技术我们要抱有积极正确的态度,它是一把双刃剑,不可滥用也不能固步自封,使用它解决问题的过程中要谨慎衡量对稳定性、业务指标等影响。
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!